interface MyElement {
  type: string | (() => MyElement)
  props: {
    children: MyElement[]
    [key: string]: any
  }
}

interface Worker extends MyElement {
  child?: Worker
  parent?: Worker
  sibling?: Worker
  dom?: HTMLElement
}

const createTextNode = (text: string): MyElement => ({
  type: 'TEXT_ELEMENT',
  props: {
    nodeValue: text,
    children: []
  }
})

const createElement = (
  type: string,
  props: { [key: string]: any },
  ...children: Array<MyElement | string>
): MyElement => ({
  type,
  props: {
    ...props,
    children: children.map((child) =>
      typeof child === 'string' || typeof child === 'number' ? createTextNode(child) : child
    )
  }
})

let nextWorkUnit: Worker | undefined
let root: Worker | undefined
const render = (el: MyElement, container: HTMLElement) => {
  nextWorkUnit = {
    dom: container,
    type: el.type,
    props: {
      children: [el]
    }
  }
  root = nextWorkUnit
}

const createDom = (type: string) =>
  type === 'TEXT_ELEMENT' ? document.createTextNode('') : document.createElement(type)

const updateProps = (dom: HTMLElement, props: { [key: string]: any }) => {
  Object.keys(props).forEach((key) => {
    if (key !== 'children') {
      dom[key] = props[key]
    }
  })
}

const initChildren = (fiber: Worker, children: MyElement[]) => {
  let prev: Worker
  children.forEach((child, index) => {
    const worker: Worker = {
      type: child.type,
      props: child.props,
      parent: fiber
    }
    if (index === 0) {
      fiber.child = worker
    } else {
      prev.sibling = worker
    }
    prev = worker
  })
}

const updateFunctionComponent = (fiber: Worker) => {
  const children = [(fiber.type as Function)(fiber.props)]
  initChildren(fiber, children)
}

const updateHostComponent = (fiber: Worker) => {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber.type as string) as any
    updateProps(fiber.dom!, fiber.props)
  }
  const children = fiber.props.children
  initChildren(fiber, children)
}

const performWorkOfUnit = (fiber: Worker): Worker | undefined => {
  const isFunctionComponent = typeof fiber.type === 'function'
  if (isFunctionComponent) updateFunctionComponent(fiber)
  else updateHostComponent(fiber)

  if (fiber.child) return fiber.child
  let next: Worker | undefined = fiber
  while (next) {
    if (next.sibling) return next.sibling
    next = next.parent
  }
}

const workLoop = (deadline) => {
  let shouldYield = false
  while (!shouldYield && nextWorkUnit) {
    nextWorkUnit = performWorkOfUnit(nextWorkUnit)
    shouldYield = deadline.timeRemaining() < 1
  }
  if (!nextWorkUnit && root) {
    commitRoot()
  }
  requestIdleCallback(workLoop)
}

const commitRoot = () => {
  commitWork(root!.child)
  root = undefined
}

const commitWork = (fiber) => {
  if (!fiber) return
  let parent = fiber.parent
  while (!parent.dom) parent = parent.parent
  if (fiber.dom) parent.dom.append(fiber.dom)
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

requestIdleCallback(workLoop)

const React = {
  render,
  createElement
}

export default React
